﻿/*
   Copyright 2011 Softisis

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using FOM.Impl.Exceptions;
using FOM.Interfaces;
using FOM.Utils;

namespace FOM.Impl
{
    /// <summary>
    /// Standard implentation of mapper manager. this is the kernel of FluentOMapper.
    /// </summary>
    /// <remarks>Copyright Softisis 2011 (France)</remarks>
    public class ManagerImpl : IManager
    {
        /// <summary>
        /// Nammed mappings
        /// </summary>
        private IDictionary<String, AbstractMappingNode> _mappings;

        private int _internalNameCounter = 0;

        /// <summary>
        /// Hold the mapping between source object and newly instancied, if so flaggued in NewInstance() call.
        /// </summary>
        private IDictionary<String, IDictionary<object, object>> _newInstancesDic;
        private readonly object _dicoLock = new object();


        /// <summary>
        /// For lock purpose.
        /// </summary>
        private readonly object _lockobj = new object();

        public IMappingNode<TSrc, TDst> RegisterMapping<TSrc, TDst>(String name)
        {
            return AddMapping((MappingNodeImpl<TSrc, TDst>)null, name);
        }

        public void RegisterMapping<TSrc, TDst>(IMappingNode<TSrc, TDst> mapping, string name)
        {
            AddMapping(mapping, name);
        }

        protected IMappingNode<TSrc, TDst> AddMapping<TSrc, TDst>(IMappingNode<TSrc, TDst> map, String name)
        {
            lock (_lockobj)
            {
                if (_mappings == null)
                    _mappings = new Dictionary<string, AbstractMappingNode>();

                if (_mappings.ContainsKey(name))
                    throw new FOMAlreadyExistsException("Mapping named " + name + " is already defined");

                if (map == null)
                    map = new MappingNodeImpl<TSrc, TDst>("", "");
                _mappings.Add(name, (AbstractMappingNode) map);
                return map;
            }
        }

        public IMappingNode<TSrc, TDst> RegisterMapping<TSrc, TDst>()
        {
            return RegisterMapping<TSrc, TDst>("Anonymous_" + (_internalNameCounter++));
        }

        public IMappingNode<T, T> RegisterMapping<T>(string name)
        {
            return RegisterMapping<T, T>(name);
        }

        public IMappingNode<T, T> RegisterMapping<T>()
        {
            return RegisterMapping<T, T>("Anonymous_" + (_internalNameCounter++));
        }

        public bool UnRegisterMapping(string name)
        {
            lock (_lockobj)
            {
                if (_mappings == null)
                    return false;

                return _mappings.Remove(name);
            }
        }

        public IMappingNode<TSrc, TDest> GetMapping<TSrc, TDest>(string mapname)
        {
            lock (_lockobj)
            {
                if (_mappings == null)
                    return null;

                if (!_mappings.ContainsKey(mapname))
                    return null;

                return (IMappingNode<TSrc, TDest>)_mappings[mapname];
            }
        }


        internal Dictionary<IPathLink, Type> AddOverrideNode(FlatMappingDescriptor fmd, Dictionary<IPathLink, Type> overridingTypingNodes)
        {
            if (overridingTypingNodes == null) // lazy init for better memory maganement
                overridingTypingNodes = new Dictionary<IPathLink, Type>();
            overridingTypingNodes.Add(fmd.FullPathLink, fmd.SourceNode.TargetOverrideType);
            return overridingTypingNodes;
        }

        /// <summary>
        /// Apply the mappping. 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="srcObject"></param>
        /// <param name="target"></param>
        /// <returns></returns>
        public object ApplyMapping(string name, object srcObject, object target)
        {
            // ultra stupid case:
            if (srcObject == null)
                return null;

            // get the mapping by its name
            AbstractMappingNode mapping = null;
            lock (_lockobj)
            {
                if (_mappings == null)
                    return null;

                if (!_mappings.ContainsKey(name))
                    throw new FOMMappingNotFound("Mapping named " + name + " was not found.");

                mapping = _mappings[name];
            }
            return ApplyMapping((INode)mapping, srcObject, target, null);

        }


        /// <summary>
        /// Here is the main code of the mapping engine.
        /// </summary>
        /// <param name="rootNode"></param>
        /// <param name="srcObject"></param>
        /// <param name="target"></param>
        /// <param name="objcache">Cache object. Provide your or use null for default instanciation.</param>
        /// <returns></returns>
        public object ApplyMapping(INode rootNode, object srcObject, object target, IDictionary<object, object> objcache)
        {

            if (objcache == null)
                objcache = new Dictionary<object, object>();
            
            AbstractMappingNode mapping = (AbstractMappingNode)rootNode;
            // collect all pathes
            var allpathes = mapping.CollectNodes(null, null); // full pathes
            
            /**********************/
            allpathes.Sort();     // !! Important. All treatments use this property to work fine. !!!
            /**********************/

            object ret = null; // root value to return (the first path processed in fact)

            // stores the overriden types for pathes (overriden is when you decide to change the type)
            Dictionary<IPathLink, Type> overridingTypingNodes = null;

            // Will be set as soon as all subpath from this must be ignored. This is used
            // for conditionnal nodes taht will not map, and for which IgnoreSubMappings is got "true".
            string ignoreChildrenOfPath = null;

            foreach (var fmd in allpathes) // for each path, ordered by path dept
            {
                try
                {
                    if (ignoreChildrenOfPath != null)
                    {
                        // we must bypass all subpaths. We use th sorted property of the pathes
                        var tmpp = PathHelper.ToCannonicalPath(fmd.FullPathLink.SourcePath);
                        if (!PathHelper.IsIncluded(ignoreChildrenOfPath, tmpp, true, false))
                            ignoreChildrenOfPath = null;
                        else
                            continue;
                    }

                    // STEP 0: If this mapping belongs to an excluded path, this is prior to the mapping itself
                    bool toIgnore = false;
                    // the parent holds the possible exclusion of some of its children
                    if (fmd.SourceNode.Parent != null && fmd.SourceNode.Parent.Excluded != null)
                    {
                        foreach (var en in fmd.SourceNode.Parent.Excluded)
                        {
                            // because we receive full pathes from the engine, we can do a direct match
                            if (fmd.FullPathLink.SourcePath == en.SourcePath &&
                                fmd.FullPathLink.TargetPath == en.TargetPath)
                            {
                                toIgnore = true;
                                break;
                            }
                        }
                    }
                    if (toIgnore)
                        continue;


                    // STEP 0-bis: If the source can't be reached, it's useless to go on
                    DiggedObjectInfos srcObjectInfos = ObjectHelper.DigAndBuild(srcObject,
                                                                                null,
                                                                                // null because we don't create missing obj
                                                                                fmd.FullPathLink.SourcePathElements,
                                                                                null,
                                                                                false);
                    if (srcObjectInfos == null) // no way
                        continue;

                    // STEP 0-ter: If the link is a goto link, don't map anything ==> we continue
                    if (fmd.SourceNode.GetType().GetGenericTypeDefinition() == typeof (GoToMappingNodeImpl<,>))
                    {
                        if (fmd.SourceNode.TargetOverrideType != null)
                        {
                            // if this node contains an overriding target type, we store it, because future new instanciations
                            // in the target path will have to use it
                            overridingTypingNodes = AddOverrideNode(fmd, overridingTypingNodes);
                        }
                        continue;
                    }


                    // determine the source type of the object to be instanciated. 
                    // This can be changed du to override capabilities
                    var targetType = fmd.SourceNode.TargetType;
                    if (fmd.SourceNode.TargetOverrideType != null)
                        targetType = fmd.SourceNode.TargetOverrideType;
                    var realTargetType = targetType;
                    if (targetType != null && (targetType.IsAbstract || targetType.IsInterface))
                    {
                        // we use the type of the target source object
                        if (srcObjectInfos.TargetedValue != null)
                            realTargetType = srcObjectInfos.TargetedValue.GetType();
                    }


                    // source type
                    var sourceType = fmd.SourceNode.SourceType;
                    if (fmd.SourceNode.SourceOverrideType != null)
                        sourceType = fmd.SourceNode.SourceOverrideType;

                    // Because Path can have a "" at the end, we must test it, and it become an upper
                    // override of type.
                    if (fmd.SourceNode.TargetOverrideType != null)
                    {
                        // "" means that the overrideNode applies to parent, so must be provided to the DigAndBuild method
                        if (fmd.FullPathLink.TargetPathElements[fmd.FullPathLink.TargetPathElements.Length - 1] == "")
                        {
                            var tmp = PathHelper.ToCannonicalPath(fmd.FullPathLink.TargetPath);
                            FlatMappingDescriptor tmpfm = new FlatMappingDescriptor(fmd.SourceNode,
                                                                                    fmd.FullPathLink.SourcePath, tmp);
                            overridingTypingNodes = AddOverrideNode(tmpfm, overridingTypingNodes);
                        }
                    }

                    // Determine the target object that will receive the mapped property,
                    // according to node parameters (new instance, type override, array, etc ...)
                    // Calling this method will create the object tree to the target if there is
                    // null reference inside. (that will probably be the case)
                    DiggedObjectInfos dstObjectInfos = ObjectHelper.DigAndBuild(target,
                                                                                realTargetType,
                                                                                fmd.FullPathLink.TargetPathElements,
                                                                                overridingTypingNodes,
                                                                                true);

                    // null is not normal ==> path not found
                    if (dstObjectInfos == null)
                        throw new FOMPathNotFoundException("Path '" + fmd.FullPathLink.SourcePath +
                                                           "' does not exist for object " +
                                                           fmd.SourceNode.TargetType + ": ");

                    // Step 2: Adress the source value (null or not), to know what to copy
                    object retainedValue = null;
                        // will hold the value that will be kept for final copy, not necerally the source


                    // Holds value of the property source (not in root case). It can differ from "retainedValue" above
                    // because retainedValue can be switched to a new instance (if so defined), but sourceValue
                    // will always hold the REAL source value
                    object sourcevalue = null;

                    if (ret == null) // root case
                    {
                        // stupid case 1 and 2: A value type or a string has been used as root value
                        // this is not really usefull (except applying strange converters), 
                        // but our kernel supports it.
                        if (srcObject.GetType().IsValueType
                            || srcObject.GetType().Equals(typeof (string)))
                        {
                            retainedValue = srcObject; // by default
                            if (fmd.SourceNode.CreateNewInstance)
                            {
                                // reuse cache management (only for object)
                                if (fmd.SourceNode.ReuseCachedInstance
                                    && !srcObject.GetType().IsValueType)
                                {
                                    // is processed before?
                                    if (!objcache.TryGetValue(srcObject, out retainedValue))
                                    {
                                        retainedValue = ObjectHelper.CreateNewInstanceFromType(realTargetType, srcObject);
                                        // and put it (because flagged as InCache
                                        objcache[srcObject] = retainedValue;
                                    }
                                }
                                else // classcal newinstance? Create it
                                    retainedValue = ObjectHelper.CreateNewInstanceFromType(realTargetType, srcObject);

                            }

                            // throw it tru the converter (if defined)
                            var tmpnewValue = retainedValue;
                            var ctx = new MappingContext(
                                fmd.SourceNode.Converter,
                                fmd.SourceNode.Condition,
                                fmd.SourceNode.CreateNewInstance,
                                fmd.SourceNode.ReuseCachedInstance,
                                fmd.SourceNode.ConvertBeforeTest,
                                sourceType, targetType,
                                srcObject, retainedValue,
                                dstObjectInfos.TargetedValue,
                                fmd.FullPathLink,
                                fmd.SourceNode.Excluded,
                                this,
                                srcObject,
                                target,
                                objcache
                                );


                            bool canApply = true;

                            if (fmd.SourceNode.Converter != null && fmd.SourceNode.Condition != null &&
                                fmd.SourceNode.ConvertBeforeTest)
                            {
                                // convert
                                tmpnewValue = fmd.SourceNode.Converter.Convert(ctx);

                                // set the converted value in the context
                                ctx.ObjectDst = tmpnewValue;

                                // call the applicability
                                canApply = fmd.SourceNode.Condition.CanApplyMapping(ctx);
                            }
                            else
                            {
                                if (fmd.SourceNode.Condition != null)
                                    canApply = fmd.SourceNode.Condition.CanApplyMapping(ctx);

                                // can apply must be true to convert
                                if (fmd.SourceNode.Converter != null && canApply)
                                    tmpnewValue = fmd.SourceNode.Converter.Convert(ctx);
                            }

                            if (canApply)
                                return tmpnewValue;
                            return null; // null if can't apply mapping
                        }

                        // In standard case, for root, the new value is the "target" object itself, 
                        // but this can be overriden by newInstance() below
                        retainedValue = dstObjectInfos.TargetedValue;
                    }
                    else // not root case. We have a property to process
                    {
                        // At the leaf?: The new value is the "object" (or V.T.) in the source
                        sourcevalue = ObjectHelper.GetValue(srcObject, fmd.FullPathLink.SourcePathElements);

                        // special case: If path element is "", this means "itself",
                        // so, we keep existing dest object as the leaf
                        if (fmd.SourceNode.NodePathLink.TargetPath == "")
                            retainedValue = dstObjectInfos.TargetedValue;
                        else
                            retainedValue = sourcevalue; // at the begining of the story, that's the case
                    }

                    bool putInCache = false; // tell later if a cache storage is needed (stored in cache )

                    // if new instance has been required, create it.
                    if (fmd.SourceNode.CreateNewInstance)
                    {
                        // if cache enabled for this field, and not value type, look in the cache
                        // management (only for object so)
                        if (fmd.SourceNode.ReuseCachedInstance && srcObjectInfos.TargetedValue != null
                            && !srcObjectInfos.TargetedValue.GetType().IsValueType)
                        {
                            // was processed before?
                            object tmpretval = null;
                            if (!objcache.TryGetValue(srcObjectInfos.TargetedValue, out tmpretval))
                            {
                                // not found? create instance
                                retainedValue = ObjectHelper.CreateNewInstanceFromType(realTargetType, retainedValue);
                                // and put it (because flagged as InCache)
                                //objcache[srcObjectInfos.TargetedValue] = retainedValue;
                                putInCache = true; // will be put after the whole porcess
                            }
                            else
                            {
                                retainedValue = tmpretval;
                            }
                        }
                        else // classical newinstance with no cache? Simply create it
                            retainedValue = ObjectHelper.CreateNewInstanceFromType(realTargetType, retainedValue);
                                // as we can see, retained value is not the sourceValue anymore
                    }

                    // The 1st one is the root case
                    if (ret == null)
                    {
                        ret = retainedValue;
                        target = ret;
                        var ctx = new MappingContext(
                            fmd.SourceNode.Converter,
                            fmd.SourceNode.Condition,
                            fmd.SourceNode.CreateNewInstance,
                            fmd.SourceNode.ReuseCachedInstance,
                            fmd.SourceNode.ConvertBeforeTest,
                            sourceType,
                            targetType,
                            srcObject,
                            ret,
                            dstObjectInfos.TargetedValue,
                            fmd.FullPathLink,
                            fmd.SourceNode.Excluded,
                            this,
                            srcObject,
                            target,
                            objcache
                            );

                        bool canApply = true;
                        // FFJ 30-11-2011: Stack order
                        if (fmd.SourceNode.Converter != null
                            && fmd.SourceNode.Condition != null
                            && fmd.SourceNode.ConvertBeforeTest)
                        {
                            // convert
                            ret = fmd.SourceNode.Converter.Convert(ctx);
                            // set the converted value in the context
                            ctx.ObjectDst = ret;
                            // test the applicability
                            canApply = fmd.SourceNode.Condition.CanApplyMapping(ctx);
                        }
                        else
                        {
                            // test the applicability
                            if (fmd.SourceNode.Condition != null)
                                canApply = fmd.SourceNode.Condition.CanApplyMapping(ctx);

                            // if a converter exist, converts the sourcevalue
                            // to set the retainedValue.
                            // If mapping can't be applicable, no conversion
                            if (fmd.SourceNode.Converter != null && canApply)
                                ret = fmd.SourceNode.Converter.Convert(ctx);
                        }

                        if (!canApply)
                            ret = null;
                        else
                        {
                            if (putInCache) // if need to put in cache
                                objcache[srcObjectInfos.TargetedValue] = ret;
                        }
                    }
                    else // the rest is the object tree
                    {
                        // before setting the value, use a possible converter
                        var finalValue = retainedValue; // before converter

                        var ctx = new MappingContext(
                            fmd.SourceNode.Converter,
                            fmd.SourceNode.Condition,
                            fmd.SourceNode.CreateNewInstance,
                            fmd.SourceNode.ReuseCachedInstance,
                            fmd.SourceNode.ConvertBeforeTest,
                            sourceType,
                            targetType,
                            sourcevalue,
                            finalValue,
                            dstObjectInfos.TargetedValue, // object taht will be replaced in the target (if any)
                            fmd.FullPathLink,
                            fmd.SourceNode.Excluded,
                            this,
                            srcObject,
                            target,
                            objcache
                            );

                        bool canApply = true;
                        // FFJ 30-11-2011: Stack order
                        if (fmd.SourceNode.Converter != null
                            && fmd.SourceNode.Condition != null
                            && fmd.SourceNode.ConvertBeforeTest)
                        {

                            // convert
                            finalValue = fmd.SourceNode.Converter.Convert(ctx);
                            // set the converted value in the context
                            ctx.ObjectDst = finalValue;
                            // test the applicability
                            canApply = fmd.SourceNode.Condition.CanApplyMapping(ctx);
                        }
                        else
                        {

                            // test the applicability
                            if (fmd.SourceNode.Condition != null)
                                canApply = fmd.SourceNode.Condition.CanApplyMapping(ctx);

                            // if a converter exist, converts the sourcevalue
                            // to set the retainedValue.
                            // If mapping can't be applicable, no conversion
                            if (fmd.SourceNode.Converter != null && canApply)
                                finalValue = fmd.SourceNode.Converter.Convert(ctx);
                        }

                        // finally, set the value in the target property of the target object
                        if (canApply)
                        {

                            dstObjectInfos.TargetProperty.SetValue(dstObjectInfos.ContainingObject, finalValue, null);
                            if (putInCache) // determined above
                                objcache[srcObjectInfos.TargetedValue] = finalValue;
                        }
                        else
                        {
                            // if the condition is recursive (i.e. applies to children mappings), notify the loop
                            if (fmd.SourceNode.Condition.IgnoreChildMappings)
                                ignoreChildrenOfPath = fmd.FullPathLink.SourcePath;
                        }
                    }
                }
                catch(Exception ex)
                {
                    throw new Exception(
                        "Can't process source path '" + fmd.FullPathLink.SourcePath + "' to '" +
                        fmd.FullPathLink.TargetPath + "'", ex);
                }
            }

            return ret;
        }

        public object ApplyMapping(string name, object src)
        {
            return ApplyMapping(name, src, null);
        }

        public object ApplyMapping(INode rootNode, object src, IDictionary<object, object> objcache)
        {
            return ApplyMapping(rootNode, src, null, objcache);
        }



        public IDictionary<object, object> GetNewInstanceDic(string mapname)
        {
            lock (_dicoLock)
            {
                if (_newInstancesDic == null)
                    return null;

                IDictionary<object, object> res;
                if (!_newInstancesDic.TryGetValue(mapname, out res))
                    return null;
                return res;
            }
        }


        public void SetNewInstanceDic(string mapname, IDictionary<object, object> dic)
        {
            lock (_dicoLock)
            {
                if (_newInstancesDic == null)
                    _newInstancesDic = new Dictionary<string, IDictionary<object, object>>();

                _newInstancesDic[mapname] = dic;
            }
        }
    }
}